package pl.edu.mimuw.mas.agent.behaviour;

import pl.edu.mimuw.mas.tools.PeriodModifier;
import jade.core.Agent;
import jade.core.behaviours.SimpleBehaviour;

/**
 * Zachowanie identyczne do TimedBehaviour z tą różnicą, że możliwa jest zmiana
 * długości tury podczas wykonywania. Zmiany dokonują się poprzez przemnożenie
 * oryginalnego czasu przez czynnik skalujący z obiektu {@link PeriodModifier}.
 */
public abstract class MutableTimedBehaviour extends SimpleBehaviour {
	private static final long serialVersionUID = 7701913569756840996L;

	private static final long BLOCK_QUANTUM = 10;
	
	private long period; // liczba milisekund bez uwzglednienia PeriodModifier
	private long wakeupTime; // czas systemowy w ktorym minie aktualny block()
	private long remainingMilis; // pozostała liczba milisekund do następnego wywyłania onTick() z uwzględnieniem PeriodModifier
	private double currentScale; // skala z jaką wyliczono mutatedRemainingTime
	private boolean finished;
	private int tickCount = 0;

	
	PeriodModifier modifier;
	
	public MutableTimedBehaviour(Agent a, long period) {
		super(a);
		this.period = period;
		this.modifier = PeriodModifier.getInstance();
		
		if (this.period <= 0) {
			throw new IllegalArgumentException("Period must be greater than 0");
		}
		if (this.modifier.getScale() < 0) {
			throw new IllegalArgumentException("Modifier scale must be greater than 0");
		}
	}
	
	public void onStart() {
		remainingMilis = getPeriod();
	}
	
	public final void action() {
		// Someone else may have stopped us in the meanwhile
		if (!finished) {
			// sprawdzam czy spałem już cały wyznaczony czas
			// jeśli nie to spię dalej
			long blockTime = wakeupTime - System.currentTimeMillis();
			if (blockTime > 0) {
				block(blockTime);
			}
			// spałem cały wyznaczony czas, wiec sprawdzam dalej
			// jeśli zmieniła się skala to przeliczam remainingTime
			if (currentScale != modifier.getScale()) {
				double previousScale = currentScale;
				currentScale = modifier.getScale();
				remainingMilis = (long) (currentScale * remainingMilis / previousScale);
			}
			// jeśli spałem już całą przerwę to robię onTick()
			if (remainingMilis <= 0) {
				// Timeout is expired --> execute the user defined action and
				// re-initialize wakeupTime
				tickCount++;
				onTick();
				remainingMilis = getPeriod();
			}
			// znowu idę spać
			if (myAgent != null && !finished && isRunnable()) {
				blockTime = Math.min(BLOCK_QUANTUM, remainingMilis);
				wakeupTime = System.currentTimeMillis() + blockTime;
				block(blockTime);
				remainingMilis -= blockTime;
			}
		}
	}
	
	private long getPeriod() {
		synchronized (modifier) {
			currentScale = modifier.getScale();
			return modifier.getModifiedPeriod(period);
		}
	}
	
	public void setPeriod(long period) {
		this.period = period;
	}
	
	public final boolean done() {
		return finished;
	}
	
	protected abstract void onTick();
	
	public void reset(long period) {
		this.reset();
		if (period <= 0) {
			throw new IllegalArgumentException("Period must be greater than 0");
		}
		this.period = period;
	}
	
	public void reset() {
		super.reset();
		finished = false;
		tickCount = 0;
	}
	
	public void stop() {
		finished = true;
	}
	
	public final int getTickCount() {
		return tickCount;
	}
	
	public void mutableBlock(long milis) {
		super.block((long) (modifier.getScale() * milis));
	}
}
